【React19】サクッと理解するuseOptimistic

【React19】サクッと理解するuseOptimistic

React 19の新機能「useOptimistic」について、いいねボタンの実装例を用いながら、非同期アクション実行中の楽観的更新の仕組みとその実践的な活用方法を解説した記事です。
Clock Icon2024.10.25

こんにちは、戸田です。
React19 からuseOptimisticという新しい hooks が登場しました。
今後使われることがある hooks だと思うのでまとめてみました。

以下が useOptimistic の公式リファレンスです。

https://ja.react.dev/reference/react/useOptimistic

useOptimistic とは

簡単な説明

useOptimisticは、ある非同期アクションが進行中の間だけ、楽観的更新をするための React フックです。具体的には、以下のようなシナリオで活用されます:

  • フォームの送信:ユーザーがフォームを送信した際、サーバーのレスポンスを待たずに入力内容を即座に UI に反映させる。
  • データの更新:ユーザーがデータを編集した際、変更を即座に反映させ、サーバーへの保存が完了した後に最終的なデータを確定する。

楽観的更新とは

useOptimistic を使う際における楽観的とは、API のデータ取得で想定する結果が返ってくるものとして考えることです。
白いボタンを押したら(本当はサーバーからレスポンスを待つ必要があるけど最終的に)赤色が変化するだろうから先に変更にしちゃえ!みたいな感じです

useOptimistic を使用して定義される state を楽観的 stateといいます

メリット

useOptimisticを使用すると実際の結果よりも先に楽観的な結果を即座に返却するため、ユーザー体験が非常に良くなります。

useOptimistic の 使い方

const [optimisticState, addOptimistic] = useOptimistic(state, updateFn);

引数

  1. state: 初期状態や、実行中のアクションが存在しない場合に返される値。
  2. updateFn(currentState, optimisticValue):
    • currentState: 現在のステート値。
    • optimisticValue: addOptimisticに渡された楽観的更新に使用する値。
    • この関数は、currentStateoptimisticValueを元に新しい楽観的ステートを返します。

返り値

  1. optimisticState: 結果としての楽観的ステート。非同期アクションがない場合はstateと同一になり、アクションが実行中の場合はupdateFnが返す値となります。
  2. addOptimistic: 楽観的な更新を行うためのディスパッチ関数。任意の型の引数optimisticValueを 1 つ受け取り、updateFnを通じてステートが更新されます。

使用例: いいねボタンの実装

具体的な使用例を通じて、useOptimisticを説明します。
以下は X(旧 Twitter)でいいね ❤️ をする際の楽観的更新の例です。

import { startTransition, useOptimistic, useState } from "react";

const sendLikeAction = async (): Promise<string> => {
  try {
    // ダミー送信(3秒 delay)
    await new Promise((resolve) => setTimeout(resolve, 3000));
    return "red";
  } catch (error) {
    console.error(error);
    return "white";
  }
};

const optimisticAction = (_currentState: string, optimisticColor: string) => {
  // ここで楽観的な処理をする
  return optimisticColor;
};

export const LikeButton = () => {
  const [color, setColor] = useState<string>("white");
  const [displayLikeColor, addOptimistic] = useOptimistic<string, string>(color, optimisticAction);

  const handleLikeClick = () => {
    // 非同期アクションが進行中と示すためにstartTransitionを使用している(useActionStateやuseTransition内でも使用可能)
    startTransition(async () => {
      // 楽観的に表示する際の色を指定(yellowになる)
      addOptimistic("yellow");
      // データを取得してから元のstateを更新
      const sendedIsLike = await sendLikeAction();
      setColor(sendedIsLike);
    });
    // transitionが終わったので楽観的な表示を解除(redになる)
  };

  return (
    <div>
      <h3>誰かの投稿</h3>
      <div>
        <span onClick={handleLikeClick} style={{ color: displayLikeColor }}></span>
      </div>
    </div>
  );
};
コメントなし
import { startTransition, useOptimistic, useState } from "react";

const sendLikeAction = async (): Promise<string> => {
  try {
    await new Promise((resolve) => setTimeout(resolve, 3000));
    return "red";
  } catch (error) {
    console.error(error);
    return "white";
  }
};

const optimisticAction = (_currentState: string, optimisticColor: string) => {
  return optimisticColor;
};

export const LikeButton = () => {
  const [color, setColor] = useState<string>("white");
  const [displayLikeColor, addOptimistic] = useOptimistic<string, string>(color, optimisticAction);

  const handleLikeClick = () => {
    startTransition(async () => {
      addOptimistic("yellow");
      const sendedIsLike = await sendLikeAction();
      setColor(sendedIsLike);
    });
  };

  return (
    <div>
      <h3>誰かの投稿</h3>
      <div>
        <span onClick={handleLikeClick} style={{ color: displayLikeColor }}></span>
      </div>
    </div>
  );
};

コードの解説

今回の例では、useOptimistic フックを使って「いいね」ボタンの色を楽観的に更新し、ユーザーに迅速なフィードバックを提供しています。以下に、コードの各部分とその動作について簡単に説明します。

動作確認動画

この動画では分かりやすくするために表示を少し変えています。

https://youtu.be/81PIFNH_Wg0

流れ

  1. useStateで初期のハートの色を"white"にする
  2. useOptimisticに 初期値となる color と、楽観的更新を行うoptimisticActionを渡す
  3. いいね(🤍)がクリックされる
  4. addOptimisticの引数に"yellow"を渡して、楽観的更新をする(ハートが黄色になる 💛)
  5. ダミー送信をする(3 秒 delay)
  6. 送信が終わった後、返却された値("red"❤️ or "white"🤍)の色に UI を更新する

startTransition を使用した理由

「useOptimistic とは」の中で以下のように説明しました。

useOptimistic はある非同期アクションが進行中の間だけ楽観的更新をするための React フックです。

startTransition の中で行われたステート更新はトランジションであると見なされます。
トランジションとしてマークされた state 更新は、他の state 更新によって中断されます。

なので3秒後に useState で更新した state(color)が displayLikeColor より優先度が高いため、color が更新された時点で displayLikeColor も更新されました。

このようにトランジション状態にするためにstartTransitionを使用しました。

また、startTransition を使わなくてもuseActionStateuseTransitionを使うことでもトランジション状態を作れます。

// useActionState
const fn = () => {
  // トランジション状態
};
const [state, formAction] = useActionState(fn, initialState, permalink);

// useTransition
const [isPending, startTransition] = useTransition();
startTransition(() => {
  // トランジション状態
});

より詳しく知りたい方はこちらの本がおすすめです。
https://zenn.dev/uhyo/books/react-concurrent-handson-2/viewer/use-starttransition

エラーハンドリングの重要性

楽観的な UI 更新では、サーバーからのレスポンスを待たずに UI を即座に更新するため、エラーが発生した場合の対処が重要です。今回の例では、エラーが発生した場合にボタンの色を白に戻し、コンソールにエラーメッセージを表示しています。実際のアプリケーションでは、ユーザーにエラーメッセージを表示するなど、さらに慎重なエラーハンドリングが求められます。

まとめ

useOptimistic フックを活用することで、非同期アクションが完了する前に UI を即座に更新し、ユーザーにスムーズな操作感を提供できます。この手法は、「いいね」ボタンだけでなく、フォームの送信やデータの編集など、さまざまなシナリオで有効に活用できます。ぜひ活用してみてください。

最後に、前回「サクッと理解する useActionState」という記事も書いたのでぜひ見てください。

https://dev.classmethod.jp/articles/react-19-understanding-use-action-state/

参考リンク

https://ja.react.dev/reference/react/useOptimistic

https://react.dev/reference/react/startTransition

https://zenn.dev/uhyo/books/react-19-new/viewer/use-optimistic

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.